package org.barad.architecture.analyzer.archpad.event.handlers;

import java.awt.Color;
import java.awt.Point;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;

import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.TransferHandler;

import org.apache.log4j.Logger;
import org.barad.architecture.analyzer.archpad.Actions;
import org.barad.architecture.analyzer.archpad.ArchPad;
import org.barad.architecture.analyzer.archpad.CustomizedJGraph;
import org.barad.architecture.analyzer.archpad.Actions.FileType;
import org.barad.architecture.analyzer.archpad.component.ArchPadComponent;
import org.barad.architecture.analyzer.archpad.component.ComponentFactory;
import org.barad.architecture.analyzer.archpad.xml.handler.ComponentDescriptor;
import org.barad.architecture.analyzer.widgets.ArchPadButton;
import org.jgraph.JGraph;
import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.DefaultPort;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;

/**
 * This class a transfer handler that handles the drag and drop 
 * operations. Such operations are drag and drop inserting of
 * new base architectural components (from the buttons in component
 * tool bar) and library components from the library tab panel.
 * 
 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
 *
 */
public class JGraphTransferHandler extends TransferHandler {
	public static final long serialVersionUID = 1L;
	private ArchPad archPad;
	private Logger log;
	private DataFlavor[] componentFlavors;
	private DefaultGraphCell sourceCell;
    private ArchPadComponent transferredObject;
    private JButton sourceComponent;

    /**
     * Instantiates a {@link JGraphTransferHandler}.
     * @param archPad A handle to the {@link ArchPad} instance.
     */
	public JGraphTransferHandler(ArchPad archPad) {
		this.archPad = archPad;
		this.log = Logger.getLogger(getClass());
		try {
			componentFlavors =  new DataFlavor[]{new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType), DataFlavor.stringFlavor};
		} catch (ClassNotFoundException cnfe) {
			/*ignore*/
		}
	}

	/**
	 * Determines if this transfer handler can import certain
	 * data flavor.
	 * 
	 * @param transferSupport {@link TransferSupport} that provides
	 *        all the required information for performing drag and drop
	 *        operations.
	 * @return True if the data flavor can be imported, false otherwise.  
	 */
	@Override
	public boolean canImport(TransferSupport transferSupport) {
		DataFlavor[] flavors = transferSupport.getDataFlavors();
		for (DataFlavor dfOut: flavors) {
			for (DataFlavor dfIn: componentFlavors) {
				if (dfOut.equals(dfIn)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * This method is implemented just to isolate the default
	 * behavior of the super class.
	 * 
	 * @see TransferHandler#importData(TransferSupport)
	 * @return Always false.
	 */
	@Override
	public boolean importData(JComponent c, Transferable t) {
		/*do nothing*/
		return false;
	}

	/**
	 * This method is implemented just to isolate the default
	 * behavior of the super class.
	 * 
	 * @see TransferHandler#canImport(TransferSupport)
	 * @return Always false.
	 */
	@Override
	public boolean canImport(JComponent c, DataFlavor[] flavors) {
		/*do nothing*/
		return false;
	}

	/**
	 * @see TransferHandler#createTransferable(JComponent)
	 */
	@Override
	protected Transferable createTransferable(JComponent c) {
    	Transferable transferable = null;
		if (c instanceof ArchPadButton) {
    		ArchPadButton button = (ArchPadButton)c;
    		if (button.isCreateTransferable()) {
	    		ArchPadButton archPadButton = (ArchPadButton)c;
	    		sourceComponent = archPadButton;
	    		ComponentDescriptor componentDescriptor = archPadButton.getComponentDescriptor();
	    		String className = componentDescriptor.getClassName();
	    		transferredObject = (ArchPadComponent)ComponentFactory.getInstance().getComponentInstance(className);
	    		transferredObject.setIconFileName(archPadButton.getIconFileName());
	    		transferredObject.setIcon(archPadButton.getIcon());
	    		transferredObject.setLinkMandatory(componentDescriptor.isLinkMandatory());
	    		transferable =  new ComponentTransferable(transferredObject);
    		}
    	} else {
    		throw new IllegalArgumentException();
    	}
        return transferable;
    }

	/**
	 * Clears the transferred object upon completion of the transfer
	 * operation.
	 * 
	 * @see TransferHandler#exportDone(JComponent, Transferable, int)
	 */
	@Override
	protected void exportDone(JComponent source, Transferable data, int action) {
		transferredObject = null;
	}

	/**
	 * This method is implemented just to isolate the default
	 * behavior of the super class.
	 * 
	 * @see TransferHandler#exportToClipboard(JComponent, Clipboard, int)
	 */
	@Override
	public void exportToClipboard(JComponent comp, Clipboard clip, int action) 
	    throws IllegalStateException {
		/*do nothing*/
	}

	/**
	 * @see TransferHandler#getSourceActions(JComponent)
	 * @return Always TransferHandler.COPY_OR_MOVE.
	 */
	@Override
	public int getSourceActions(JComponent c) {
		return TransferHandler.COPY_OR_MOVE;
	}

	/**
	 * @see TransferHandler#getVisualRepresentation(Transferable)
	 * @return The icon of the transferred component.
	 */
	@Override
	public Icon getVisualRepresentation(Transferable t) {
		return sourceComponent.getIcon();
	}

	/**
	 * Imports an ArchPad component from an {@link ArchPadButton} or
	 * or a component file.
	 * 
	 * @return True if the import succeeded, false otherwise.
	 * 
	 * @see TransferHandler#importData(TransferSupport)
	 */
	@Override
	public boolean importData(TransferSupport transferSupport) {
		boolean result = false;
		if (canImport(transferSupport)) {
			try {
				Transferable transferable = (Transferable)transferSupport.getTransferable();
				DataFlavor dataFlavor = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType);
				try {
					importComponentFromButton(transferable, transferSupport, dataFlavor);
					result = true;
				} catch (UnsupportedFlavorException ufe) {
					importComponentFromFile(transferable);
					result = true;
				}
			} catch (Exception e) {
				log.error("Error during insertion a new vertex in the graph", e);
			}
		}
		return result;
	}
	
	/*
	 * Imports a component from a file. The file name is contained as transfer
	 * data in the transferable instance and the path is obtained by the ArchPad
	 * {@link Actions} instance.
	 * 
	 * @param transferable The instance that contains the transfer data.
	 * @throws IOException - if the data is no longer available in the requested flavor. 
     *         UnsupportedFlavorException - if the requested data flavor is not supported.
	 */
	private void importComponentFromFile(Transferable transferable) throws IOException, 
	        UnsupportedFlavorException {
		Actions actions = archPad.getActions();
		String transferData = (String)transferable.getTransferData(DataFlavor.stringFlavor);
		File file = new File(actions.getComponentLibraryPath(), transferData);
		Object[] cells = actions.getComponentLibraryCache().get(file.getName());
		if (cells == null) {
			actions.loadFromFile(FileType.COMPONENT, file);
		}
		cells = actions.getComponentLibraryCache().get(file.getName());
		GraphLayoutCache cache = archPad.getCustomizedJGraph().getGraphLayoutCache();
		cache.insert(cells);
		cache.collapse(new Object[]{cells[0]});
	}
	
	/*
	 * Imports a component from a component tool bar button. The file name 
	 * is contained as transfer data in the transferable instance.
	 * 
	 * @param transferable The instance that contains the transfer data.
	 * @throws IOException - if the data is no longer available in the requested flavor. 
     *         UnsupportedFlavorException - if the requested data flavor is not supported.
	 */
	private void importComponentFromButton(Transferable transferable, TransferSupport 
			transferSupport, DataFlavor dataFlavor) throws IOException, UnsupportedFlavorException {
		Object transferData = transferable.getTransferData(dataFlavor);
		ArchPadComponent archPadComponent = (ArchPadComponent)transferData;
		DefaultGraphCell cell = new DefaultGraphCell(archPadComponent);
		Point point = transferSupport.getDropLocation().getDropPoint();
		GraphConstants.setBounds(cell.getAttributes(), new Rectangle2D.Double(point.getX(), point.getY(), 70, 70));
		GraphConstants.setIcon(cell.getAttributes(), archPadComponent.getIcon());
		GraphConstants.setBorder(cell.getAttributes(), BorderFactory.createLineBorder(Color.BLACK));
		archPad.getCustomizedJGraph().getGraphLayoutCache().insert(cell);
		final JGraph jGraph = archPad.getCustomizedJGraph();
		//add a mouse listener which first consumes the mouse event. In case it does not process it
		//the event is dispatched to the rest of the mouse event listeners
		addJGraphMouseListener(jGraph);
		addJGraphMouseMotionListener(jGraph);
	}
	
	/**
	 * This method removes all {@link MouseListener} event listeners from the
	 * {@link JGraph} instance and adds a custom {@link MouseListener} which contains
	 * handles to the removed event listeners. It delegates to these listeners 
	 * after processing an event. Swing does not guarantee the order of invocation
	 * of the registered event listeners but in certain cases {@link ArchPad}
	 * requires to handle the event and swallow it to avoid execution of 
	 * the default listeners.
	 * 
	 * @param jGraph A {@link JGraph} instance.
	 */
	private void addJGraphMouseListener(JGraph jGraph) {
		final MouseListener[] mouseListenerz = jGraph.getMouseListeners();
		for (MouseListener ml: mouseListenerz) {
			jGraph.removeMouseListener(ml);
		}
		jGraph.addMouseListener(new MouseListener() {
			private MouseListener[] mouseListeners = mouseListenerz;

			/**
			 * @see MouseListener#mouseClicked(MouseEvent)
			 */
			public void mouseClicked(MouseEvent e) {
				if (mouseListeners != null) {
					for(MouseListener ml: mouseListeners) {
						ml.mouseClicked(cloneMouseEvent(e));
					}
				}
			}

			/**
			 * @see MouseListener#mouseEntered(MouseEvent)
			 */
			public void mouseEntered(MouseEvent e) {
				if (mouseListeners != null) {
					for(MouseListener ml: mouseListeners) {
						ml.mouseEntered(cloneMouseEvent(e));
					}
				}
			}

			/**
			 * @see MouseListener#mouseExited(MouseEvent)
			 */
			public void mouseExited(MouseEvent e) {
				if (mouseListeners != null) {
					for(MouseListener ml: mouseListeners) {
						ml.mouseExited(cloneMouseEvent(e));
					}
				}
			}

			/**
			 * If ArchPad is in a link drawing mode and if there is a 
			 * JGraph cell under the point of the mouse event the cell is
			 * set as the source cell for drawing an component link.
			 * 
			 * @see MouseListener#mouseExited(MouseEvent)
			 */
			public void mousePressed(MouseEvent e) {
				if (archPad.isLinkDrawingEnabled()) {
					JGraph jGraph = (JGraph)e.getComponent();
					DefaultGraphCell cell = (DefaultGraphCell)jGraph.getFirstCellForLocation(e.getX(), 
							e.getY());
					if (cell != null) {
						Rectangle2D cellBounds = jGraph.getCellBounds(cell);
						boolean mouseInCellBounds = cellBounds.contains(new Point(e.getX(), e.getY()));
						if (mouseInCellBounds) {
							sourceCell = cell;
						}
					}
				} else {
					if (mouseListeners != null) {
						for(MouseListener ml: mouseListeners) {
							ml.mousePressed(cloneMouseEvent(e));
						}
					}
				}
			}

			/**
			 * If ArchPad is in a link drawing mode and if there is a 
			 * JGraph cell under the point of the mouse event and there is
			 * a cell that denotes a link beginning a link between the
			 * graph cells is created. Note that a link will be drawn
			 * only if at least one of the components is required to have
			 * links (ArchPadComponent#isLinkMandatory()).
			 * 
			 * @see MouseListener#mouseReleased(MouseEvent)
			 */
			public void mouseReleased(MouseEvent e) {
				if (archPad.isLinkDrawingEnabled()) {
					CustomizedJGraph jGraph = (CustomizedJGraph)e.getComponent();
					DefaultGraphCell cell = (DefaultGraphCell)jGraph.getFirstCellForLocation(e.getX(), 
							e.getY());
					if (sourceCell != null) {
						if (cell != null && cell != sourceCell) {
							ArchPadComponent fromComponent = (ArchPadComponent)sourceCell.getUserObject();
							ArchPadComponent toComponent = (ArchPadComponent)cell.getUserObject();
							if (fromComponent.isLinkMandatory() || toComponent.isLinkMandatory()) {
								sourceCell.add(new DefaultPort());
								cell.add(new DefaultPort());
								DefaultEdge edge = new DefaultEdge();
								edge.setSource(sourceCell.getChildAt(0));
								edge.setTarget(cell.getChildAt(0));
								jGraph.getGraphLayoutCache().insert(edge);
							} else {
								String message = "Direct link between " + fromComponent.getName() + " and " 
								+ toComponent.getName() + " components in not allowed by the model";
								JOptionPane.showMessageDialog(null, message, "Model constraint violation", 
										JOptionPane.INFORMATION_MESSAGE);
							}
						}
						jGraph.setFrom(null);
						jGraph.setTo(null);
						jGraph.repaint();
					}
					sourceCell = null;
				} else {
					if (mouseListeners != null) {
						for(MouseListener ml: mouseListeners) {
							ml.mouseReleased(cloneMouseEvent(e));
						}
					}
				}
			}

			/*
			 * Clones a mouse event.
			 * @param e The event to be cloned.
			 * @return The clone.
			 */
			private MouseEvent cloneMouseEvent(MouseEvent e) {
				MouseEvent clone = new MouseEvent(e.getComponent(), e.getID(), 
						e.getWhen(), e.getModifiers(), e.getX(), e.getY(), e.getClickCount(), 
						e.isPopupTrigger());
				return clone;
			}
		});
	}
	
	/**
	 * This method removes all {@link MouseMotionListener} event listeners from the 
	 * {@link JGraph}instance and adds a custom {@link MouseMotionListener} which 
	 * contains handles to the removed event listeners. It delegates to these listeners 
	 * after processing an event. Swing does not guarantee the order of invocation
	 * of the registered event listeners but in certain cases {@link ArchPad}
	 * requires to handle the event and swallow it to avoid execution of 
	 * the default listeners.
	 * 
	 * @param jGraph A {@link JGraph} instance.
	 */
	private void addJGraphMouseMotionListener(JGraph jGraph) {
		final MouseMotionListener[] motionListeners = jGraph.getMouseMotionListeners();
		for (MouseMotionListener ml: motionListeners) {
			jGraph.removeMouseMotionListener(ml);
		}
		jGraph.addMouseMotionListener(new MouseMotionListener() {
			private MouseMotionListener[] mouseMotionListeners = motionListeners;

			/**
			 * This method draws a line between the JGraph cell that is a source
			 * of a currently drawn link and the current mouse position. If the
			 * source is not defined no line is drawn. 
			 * 
			 * @see MouseMotionListener#mouseDragged(MouseEvent)
			 */
			public void mouseDragged(MouseEvent e) {
				if (archPad.isLinkDrawingEnabled()) {
					if (sourceCell != null) {
						CustomizedJGraph jGraph = archPad.getCustomizedJGraph();
						Rectangle2D cellBounds = jGraph.getCellBounds(sourceCell);
						Point2D center = new Point2D.Double(cellBounds.getCenterX(), 
								cellBounds.getCenterY());
						jGraph.setFrom(center);
						jGraph.setTo(new Point2D.Double(e.getX(), e.getY()));
						jGraph.repaint();
					}
				} else {
					if (mouseMotionListeners != null) {
						for(MouseMotionListener ml: mouseMotionListeners) {
							ml.mouseDragged(cloneMouseEvent(e));
						}
					}
				}
			}

			/**
			 * @see MouseMotionListener#mouseMoved(MouseEvent)
			 */
			public void mouseMoved(MouseEvent e) {
				if (mouseMotionListeners != null) {
					for(MouseMotionListener ml: mouseMotionListeners) {
						ml.mouseMoved(cloneMouseEvent(e));
					}
				}
			}

			/*
			 * Clones a mouse event.
			 * @param e The event to be cloned.
			 * @return The clone.
			 */
			private MouseEvent cloneMouseEvent(MouseEvent e) {
				MouseEvent clone = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), 
						e.getModifiers(), e.getX(), e.getY(), e.getClickCount(), 
						e.isPopupTrigger());
				return clone;
			}
		});
	}
	
	/**
	 * This class represents a transfer object for JGraph component.
	 *  
     * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
	 */
	private class ComponentTransferable implements Transferable {
	    private Logger log;
		private Object object;
	    private DataFlavor componentFlavor;

	    /**
	     * Creates a new instance given an instance of the object to
	     * be transferred.
	     * 
	     * @param object The instance to be transferred.
	     */
	    public ComponentTransferable(Object object) {
	    	log = Logger.getLogger(getClass());
	        this.object = object;
	        try {
	        	this.componentFlavor = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType);
	        } catch (ClassNotFoundException cnfe) {
	        	log.error("No such data flavor", cnfe);
	        }
	    }

	    /**
	     * @see Transferable#getTransferData(DataFlavor)
	     */
	    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
	        if (!isDataFlavorSupported(flavor)) {
	            throw new UnsupportedFlavorException(flavor);
	        }
	        return object;
	    }

	    /**
	     * @see Transferable#getTransferDataFlavors()
	     */
	    public DataFlavor[] getTransferDataFlavors() {
	        return new DataFlavor[] {componentFlavor};
	    }

	    /**
	     * @see Transferable#isDataFlavorSupported(DataFlavor)
	     */
	    public boolean isDataFlavorSupported(DataFlavor flavor) {
	        return componentFlavor.equals(flavor);
	    }
	}
}
